iT邦幫忙

2022 iThome 鐵人賽

DAY 5
0
Mobile Development

Android app 效能優化系列 第 5

縮短 App 的啟動時間

  • 分享至 

  • xImage
  •  

開啟一個 App 要花多少時間是使用者最有感的。如果開啟時間過慢,使用者就可能會給予負評或是直接移除App。

App的啟動階段

當我們說開啟 App,指的可能有這幾種狀況

  1. 安裝完 App 後的第 1 次開啟或是將 App 完全終止後的再次開啟。
  2. 重新開啟 App,但 Activity 因為已被銷毀而需要重新產生。
  3. 離開到別的 App 後再次回來(從背景回到前景)。

依照這 3 種情況的開啟 App,我們將其分為 Cold start、Warm start、Hot start。

Cold start 是指從Application 產生到Activity 顯示畫面的階段。會發生在安裝完App的第1次啟動或是 App 從完全終止再次開啟的階段。

Warm start 是指 Application 已經產生,Activity 需要建立。會發生在重新打開App時因為Activity已被銷毀而再次產生 Activity 時。

Hot start 是指暫時離開 App,又再回來時所需要的時間。也就是從 App 背景被喚起到前景,所以 Hot start 通常會從 Activity 的 onStart 開始。

我們可以從這張 Android 官方比較圖看得出來彼此的關係。
App startup phases
圖片來源:https://medium.com/androiddevelopers/important-performance-metrics-c7dacf018eb3

而當我們要衡量啟動時間,可以分別取得兩個指標「初始顯示時間」、「顯示完整內容時間」來觀察App 的啟動是否太慢。

初始顯示時間 The time to initial display (TTID)

初始顯示時間是指從 Application 的建立到 Activity 完成 onCreate 的時間。ActivityTaskManager 會自動記錄初始顯示時間,在 Logcat 搜尋 Displayed 就可以看到花了多少時間。一般來說,這裡的啟動時間應該在 500 毫秒內。

Displayed evan.chen.tutorial.startupsample/.MainActivity: +368ms

顯示完整內容時間 The Time to full display (TTFD)

顯示完整內容時間是指使用者看到完整內容的時間。假設 App 一開啟就會從後端 API 取得資料後繫結到 UI 顯示。我們會在 Activity 的 onCreate 非同步呼叫後端 API 後再將取得的資料顯示在畫面上。像這樣的處理時間是不被計算在初始顯示時間的。所以我們需要另一個是「顯示完整內容時間」來表達使用者真的看到內容的時間。所以這個時間會依每個 App 的使用情境有所不同,你需要在使用者看到資料的地方自行呼叫 reportFullyDrawn

viewModel.data.observe(this) {
    // 取得後端資料申,在這裡呼叫reportFullyDrawn表示已經顯示完整的內容了。
	reportFullyDrawn()
}

在 Logcat 就會看到顯示完整內容時間

Fully drawn MainActivity: +5s907ms

顯示完整內容時間的優化,需要從 Layout 編排、Cache 機制、介面的設計做效能的優化,在之後我們會做詳細的介紹。這一篇我們要來介紹的是如何優化初始顯示時間。

初始顯示時間的優化

初始顯示時間如果花費太多時間,可能的原因是 Application 的初始化與 Activity 的初始化做了太多事。

Application 的初始化

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // Application init
    }
}

Activity 的初始化

class MainActivity : AppCompatActivity() {
    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

       // Activity init
    }
}

先確認在 Application 初始化所做的事,是真的需要在這個階段就處理嗎?是否可放到載入頁面後或使用者的某個操作後再處理,以及是否可以被延遲初始化。如果仍有工作需在 Application 初始化,就可以使用 Startup library 來加快 App 啟動速度。

App Startup library

在 Android Jetpack 裡的 Startup library 就提供了一種透過 Content provider 的機制來加速 App 的開啟速度。

來看範例,我們有一個要在 App 一開啟就要載入的 Library 叫 MyLibrary

object MyLibrary {
    fun initLibrary(context: Context){
        println("init library")
    }
}

一般的做法就是直接在 ApplicationonCreate 時來做初始化

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        //初始化MyLibrary
        MyLibrary.initLibrary(this)
    }
}

接著要示範改為使用 Startup library,首先加入 dependencies:

implementation 'androidx.startup:startup-runtime:1.1.1'

新增 MyInitializer類別並繼承Initializer。在這個 MyInitializer 有兩個 override function。

第1 個: 在create 初始化 MyLibrary。

第2個:在dependencies 處理多個 Library 的載入且互有依賴時的狀況。

class MyInitializer : Initializer<MyLibrary> {
    override fun create(context: Context): MyLibrary {
        MyLibrary.init(context)
        return MyLibrary
    }

    override fun dependencies(): MutableList<Class<out Initializer<*>>> {
        return mutableListOf()
    }
}

新增完 MyInitializer,需要在 AndroidManifest.xml 宣告使用。

<provider
    android:authorities="${applicationId}.androidx-startup"
    android:name="androidx.startup.InitializationProvider"
    android:exported="false"
    tools:node="merge">
    <meta-data
        android:name="evan.chen.tutorial.startupsample.MyInitializer"
        android:value="androidx.startup"/>
</provider>

這樣就完成了使用 Startup library 來縮短 App 的啟動時間。

Dependencies

如果有多個 Library 要透過 Startup 來載入,彼此又互有依賴關系,就可以透過 dependencies 來處理。

建立一個新的 MyInitializer2 用來載入 MyLibrary2

class MyInitializer2 : Initializer<MyLibrary2> {
    override fun create(context: Context): MyLibrary2 {
        MyLibrary2.init(context)
        return MyLibrary2
    }

    override fun dependencies(): MutableList<Class<out Initializer<*>>> {
        return mutableListOf()
    }
}

object MyLibrary2 {
    fun init(context: Context){
        println("init library2")
    }
}

在原本的 MyInitializerdependencies 使用 mutableListOf(MyInitializer2::class.java),就可以確保在 MyInitializer 初始化 MyLibrary 之前 ,先初始化 MYLibrary2

class MyInitializer : Initializer<MyLibrary> {
    override fun create(context: Context): MyLibrary {
        MyLibrary.init(context)
        return MyLibrary
    }
    override fun dependencies(): MutableList<Class<out Initializer<*>>> {
        //需要先載入MyInitializer2
        return mutableListOf(MyInitializer2::class.java)
    }
}

最後,App 的啟動時間是很值得花時間來改善的,因為這是使用者對你的 App 的第一感受。尤其在當你在啟動時需要載入第三方元件時,就可以用 Startup 來縮短 App 啟動的時間。

參考:

https://developer.android.com/topic/performance/vitals/launch-time
https://developer.android.com/topic/libraries/app-startup


上一篇
使用 StrictMode 找出在主執行緒的異常請求
下一篇
精簡 App 的大小
系列文
Android app 效能優化30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言